默认布局:头像菜单组件
头像菜单(AvatarMenu)是 Header 组件中最复杂的子组件,它需要同时处理头像显示、用户名展示、下拉菜单渲染、事件回调等多种功能。这个组件的核心挑战在于 TypeScript 类型系统——如何从 Element Plus 的 Vue 组件实例中提取 Props 类型,以及如何处理 size 等属性名冲突。
组件结构概览
<!-- avatar-menu.vue -->
<template>
<div class="flex items-center mr-4">
<el-dropdown v-bind="menuProps" @command="handleCommand">
<div class="flex items-center">
<el-avatar v-bind="avatarProps">
{{ username?.charAt(0).toUpperCase() }}
</el-avatar>
<span v-if="username" class="ml-2">{{ username }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<template v-for="menu in data" :key="menu.key ?? menu">
<el-divider
v-if="menu.key === 'divider'"
class="my-0!"
/>
<el-dropdown-item v-else :command="getCommand(menu)">
{{ getLabel(menu) }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
vue
TypeScript 类型推导的两种方法
Element Plus 的 Dropdown 组件导出的是 Vue 组件实例,而不是 TypeScript interface。如何获取其 Props 类型?
方法一:AI 工具转换
将 Element Plus 的 buildProps 返回值(Vue 3 的 props 定义格式)交给 AI 工具(如 Cursor),让它转换成 TypeScript interface:
Prompt: "上面是 Vue 3 的 props 定义,
我需要一个 interface with TypeScript 定义,
请帮我转换一下"
text
方法二:ComponentCustomProperties(推荐)
利用 Vue 内置的类型推导工具:
import type { ComponentCustomProperties } from 'vue'
import { ElDropdown } from 'element-plus'
// 从组件实例中推导 Props 类型
type DropMenuProps = Partial<
ComponentCustomProperties &
typeof ElDropdown
>
typescript
这种方式更简洁,直接从 Element Plus 导出的组件对象中推导类型。但需要注意:withDefaults 对这种推导类型不生效,需要手动设置默认值。
size 属性冲突的处理
el-dropdown 和 el-avatar 都有 size 属性,但含义不同:
| 组件 | size 含义 | 可选值 |
|---|---|---|
| el-dropdown | 下拉菜单尺寸 | large, default, small |
| el-avatar | 头像尺寸 | number, large, default, small |
四种解决方案:
方案一:Omit 排除
interface AvatarMenuProps
extends DropMenuProps,
Omit<AvatarProps, 'size'> {
avatarSize?: number | 'large' | 'default' | 'small'
}
typescript
方案二:重命名
interface DropMenuProps {
menuSize?: 'large' | 'default' | 'small'
// ...其他属性
}
typescript
方案三:交叉类型
type AvatarMenuProps = DropMenuProps & AvatarProps
typescript
前提是两个 size 类型兼容(此处不兼容,所以不适用)。
方案四:computed 分离(推荐)
const { size: menuSize, ...restMenuProps } = props
const { size: avatarSize, ...restAvatarProps } = props
const menuProps = computed(() => ({
...restMenuProps,
size: menuSize,
}))
const avatarProps = computed(() => ({
...restAvatarProps,
size: avatarSize ?? 'small',
}))
typescript
头像显示逻辑
当用户只设置了 username 而没有设置 src(头像图片 URL)时,el-avatar 内部显示用户名的首字母大写:
<el-avatar v-bind="avatarProps" :size="avatarSize">
{{ username?.charAt(0).toUpperCase() }}
</el-avatar>
vue
el-avatar 的渲染优先级:src(图片)> icon(图标)> 插槽内容(首字母)。当设置了 src 时,插槽内容会被自动覆盖,无需手动控制显隐。
下拉菜单项的数据结构
type DropDownMenuItem =
| string
| number
| { key: string; value: string }
typescript
支持三种格式:
const menuData: DropDownMenuItem[] = [
'个人中心',
{ key: 'settings', value: '系统设置' },
{ key: 'divider', value: '' },
'退出登录',
]
typescript
渲染时根据 key === 'divider' 来决定显示分割线还是菜单项:
<el-divider
v-if="menu.key === 'divider'"
class="my-0!"
/>
<el-dropdown-item v-else>
{{ typeof menu === 'object' ? menu.value : menu }}
</el-dropdown-item>
vue
el-divider 的默认 margin 较大,使用 my-0!(Tailwind 的 !important 模式)重置为 0。
command 事件的类型定义
const emit = defineEmits<{
command: [value: string | number | object]
}>()
const handleCommand = (command: string | number | object) => {
emit('command', command)
}
typescript
事件从 AvatarMenu 发出,经过 Header 透传,最终在 DefaultLayout 中处理。
本节小结
- 类型推导:使用
ComponentCustomProperties从 Element Plus 组件实例推导 Props 类型。 - 属性冲突:
size等同名属性通过 Omit 排除或 computed 分离解决。 - 头像显示:无图片时显示首字母,利用
el-avatar的插槽机制自动处理优先级。 - 分割线:通过数据中的
key: 'divider'标记分割线位置。
↑